MariaDB기반 MultiDomain
목차
MariaDB기반 Multi Domain #
파일시스템기반 multi domain설정을 mariaDB기반 multi domain설정으로 변경하는 과정을 기록 했습니다. 누락된 정보가 있을 수 있습니다. ai에게 도움을 받으면 부족한 부분을 충분히 채울 수 있으리라 생각 합니다. 여기까지 오는데 ai의 도움을 많이 받았습니다. 초보 분들이 메일서버 설정에서 ai와 무한 루프에 빠지는 일이 없기를 바라는 마음으로 기록 남깁니다. 도움이 되기를 바랍니다.
대략적인 구조입니다 #
Postfix Mysql 구조
Postfix
│
├─ mysql-virtual-domains.cf ──► virtual_domains
│
├─ mysql-virtual-users.cf ──► virtual_users
│
├─ mysql-virtual-aliases.cf ──► virtual_aliases
│
├─ mysql-sender-canonical.cf ──► sender_canonical
│
└─ mysql-virtual-sender-maps.cf ──► virtual_users
MariaDB
/ | \
virtual_domains
virtual_users
virtual_aliases
sender_canonical
│
│
Postfix
│
│ LMTP
│
Dovecot
│
│
Maildir
각 .cf 파일은 Postfix가 “이 조건일 때 MariaDB에 어떤 쿼리를 날려서 답을 받아올지"를 정의하는 연결 설정입니다. Postfix 자체는 DB를 직접 모르고, 이 파일들을 통해서만 MariaDB와 대화합니다.
새로 생성하거나 수정할 파일목록입니다 #
sudo nano /etc/dovecot/conf.d/10-ssl.conf
sudo nano /etc/dovecot/dovecot-sql.conf.ext
sudo nano /etc/postfix/mysql-sender-canonical.cf
sudo nano /etc/postfix/mysql-virtual-domains.cf
sudo nano /etc/postfix/mysql-virtual-users.cf
sudo nano /etc/postfix/mysql-virtual-aliases.cf
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
sudo nano /etc/postfix/master.cf
sudo nano /etc/postfix/main.cf
sudo nano /etc/dovecot/dovecot.conf
sudo nano /etc/dovecot/conf.d/10-mail.conf
sudo nano /etc/dovecot/conf.d/10-master.conf
sudo nano /etc/dovecot/conf.d/20-lmtp.conf
sudo nano /etc/dovecot/conf.d/10-auth.conf
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
기본 시스템 및 MariaDB 준비 #
MariaDB 설치
sudo apt update
sudo apt install mariadb-server mariadb-client -y
데이터베이스의 자물쇠 설치 (초기 보안 설정 마법사)
sudo mysql_secure_installation
- 처음 설치 시 첫 항목 그냥 enter
Switch to unix_socket authentication?→ nChange root password?→ y (password 입력)Remove anonymous users?→ yDisallow root login remotely?→ yRemove test database?→ yReload privilege tables now?→ y
Postfix에게 주소록 연결을 위한 설치
sudo apt install postfix-mysql -y
Dovecot에게 사용자 정보 연결을 위한 설치
sudo apt install dovecot-mysql -y
메일을 전송하는 효율적인 통로 설치 (LMTP)
sudo apt install dovecot-lmtpd -y
MariaDB 시작
sudo systemctl daemon-reload
sudo systemctl enable mariadb
sudo systemctl start mariadb
sudo systemctl status mariadb
메일 전용 DB 및 계정 생성 #
Postfix/Dovecot은 조회(SELECT)만 하면 되므로 mailuser는 읽기 권한만, 관리 스크립트나 웹 UI에서 쓰는 mailadmin은 쓰기 권한까지 분리해서 부여합니다. 이렇게 권한을 나눠두면 메일 처리 경로 쪽 코드에 문제가 생겨도 DB 내용이 함부로 바뀌지 않습니다.
CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'mailuser'@'127.0.0.1' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1';
CREATE USER 'mailuser'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'localhost';
CREATE USER 'mailadmin'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mailserver.* TO 'mailadmin'@'localhost';
FLUSH PRIVILEGES;
DB 접속 후 확인
mariadb -u root -p
USE mailserver;
-- 현재 어떤 계정으로 접속했는지 확인
SELECT USER(), CURRENT_USER();
-- mailserver DB가 실제로 생성됐는지 확인
SHOW DATABASES LIKE 'mailserver';
-- 전체 DB 계정 목록
SELECT Host, User FROM mysql.user;
-- mailadmin 계정만 확인
SELECT Host, User FROM mysql.user WHERE User = 'mailadmin';
-- mailadmin에게 부여된 권한 확인
SHOW GRANTS FOR 'mailadmin'@'localhost';
-- 인증 방식(plugin) 확인
SELECT Host, User, plugin, authentication_string FROM mysql.user;
테이블 구조 설계 #
CREATE TABLE virtual_domains (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE virtual_users (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
quota BIGINT NOT NULL DEFAULT 0,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY idx_domain_active (domain_id, active),
KEY idx_email_active (email, active),
CONSTRAINT fk_users_domain
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE virtual_aliases (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
KEY idx_source_active (source, active),
KEY idx_domain (domain_id),
KEY idx_destination (destination),
CONSTRAINT fk_alias_domain
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS sender_canonical (
id INT AUTO_INCREMENT PRIMARY KEY,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_source (source),
KEY idx_active (active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
active 컬럼으로 계정/도메인 활성화 관리 가능. (삭제 대신 active=0으로 바꾸면 이력이 남아서 복구가 쉽습니다.)
기존 파일 데이터 DB 마이그레이션 #
도메인 입력
INSERT INTO virtual_domains(name, active) VALUES
('domain1.com', 1),
('domain2.com', 1),
('domain3.com', 1),
('domain4.com', 1),
('domain5.com', 1),
('domain6.com', 1),
('domain7.com', 1),
('domain8.com', 1);
도메인 전체 목록 확인
SELECT * FROM virtual_domains;
특정 도메인이 활성화 상태인지 확인
SELECT name FROM virtual_domains WHERE name='domain1.com' AND active=1;
사용자 입력 (Dovecot의 SHA512-CRYPT 해시를 그대로 사용)
INSERT INTO virtual_users (domain_id, email, password) VALUES
(1, 'user1@domain1.com', '{SHA512-CRYPT}hash1'),
(1, 'user2@domain1.com', '{SHA512-CRYPT}hash2'),
(2, 'user1@domain2.com', '{SHA512-CRYPT}hash3'),
(2, 'user2@domain2.com', '{SHA512-CRYPT}hash4'),
(3, 'user1@domain3.com', '{SHA512-CRYPT}hash5'),
(3, 'user2@domain3.com', '{SHA512-CRYPT}hash6'),
(4, 'user1@domain4.com', '{SHA512-CRYPT}hash7'),
(4, 'user2@domain4.com', '{SHA512-CRYPT}hash8')
/* ... */;
시스템 계정인 root가 메일을 발송하면, 외부에는 master@domain1.com이 보낸 것으로 표시됩니다.
INSERT INTO sender_canonical (source, destination, active)
VALUES
('root', 'master@domain1.com', 1),
('root@domain1.com', 'master@domain1.com', 1);
시스템 내부에서 자동으로 생성되는 메일들의 “보낸 사람” 주소가 master@domain1.com로 바뀝니다.
INSERT INTO sender_canonical (source, destination, active)
VALUES
('www-data', 'master@domain1.com', 1),
('postfix', 'master@domain1.com', 1),
('vmail', 'master@domain1.com', 1),
('cron', 'master@domain1.com', 1);
INSERT INTO virtual_aliases (domain_id, source, destination) VALUES
(1, 'info@domain1.com', 'master@domain1.com'),
(1, 'admin@domain1.com', 'master@domain1.com'),
(1, 'webmaster@domain1.com', 'master@domain1.com'),
(1, 'postmaster@domain1.com', 'dmarc@domain1.com'),
(2, 'info@domain2.com', 'master@domain2.com'),
(3, 'info@domain3.com', 'master@domain3.com'),
(4, 'info@domain4.com', 'master@domain4.com'),
(5, 'info@domain5.com', 'master@domain5.com'),
(6, 'info@domain6.com', 'master@domain6.com'),
(7, 'info@domain7.com', 'master@domain7.com'),
(8, 'info@domain8.com', 'master@domain8.com');
전체를 리스트로 보고 싶을 때 사용합니다.
SELECT * FROM virtual_aliases;
\G 옵션을 사용하면 각 행을 세로로 깔끔하게 나열해 줍니다.
SELECT * FROM virtual_aliases\G
SELECT DISTINCT source FROM virtual_aliases ORDER BY source;
특정 도메인의 별칭만 확인하기
SELECT * FROM virtual_aliases
WHERE source LIKE '%@domain1.com';
SELECT * FROM sender_canonical;
SELECT * FROM sender_canonical WHERE source = 'vmail';
단독 조회 테스트 (postmap으로 실제 Postfix가 보는 것과 동일한 방식으로 쿼리 결과 확인)
sudo postmap -q root mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q root@domain1.com mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q nobody mysql:/etc/postfix/mysql-sender-canonical.cf
Postfix + MariaDB 연동 #
MariaDB용 Postfix CF 파일 생성
sudo nano /etc/postfix/mysql-virtual-domains.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-users.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-aliases.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-sender-canonical.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM sender_canonical WHERE source='%s' AND active=1
Dovecot + MariaDB 연동 #
sudo nano /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=password
default_pass_scheme = SHA512-CRYPT
password_query = \
SELECT email as user, password \
FROM virtual_users \
WHERE email='%u' AND active=1
user_query = \
SELECT '/var/mail/vhosts/%d/%n' as home, 1005 as uid, 1005 as gid \
FROM virtual_users \
WHERE email='%u' AND active=1
권한 설정 #
Postfix/Dovecot이 읽어야 하는 설정 파일에는 비밀번호가 평문으로 들어있으므로, 해당 서비스 그룹만 읽을 수 있게 권한을 좁혀둡니다.
sudo chown root:postfix /etc/postfix/mysql-virtual-*.cf
sudo chmod 640 /etc/postfix/mysql-virtual-*.cf
sudo chown root:postfix /etc/postfix/mysql-sender-canonical.cf
sudo chmod 640 /etc/postfix/mysql-sender-canonical.cf
sudo chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
sudo chmod 640 /etc/dovecot/dovecot-sql.conf.ext
Postfix에 방금 만든 연동 파일들을 등록합니다.
sudo postconf -e "sender_canonical_maps = mysql:/etc/postfix/mysql-sender-canonical.cf"
sudo postconf -e "virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf"
sudo postconf -e "virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-users.cf"
sudo postconf -e "virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf"
sudo postconf -e "smtpd_sender_login_maps = mysql:/etc/postfix/mysql-virtual-sender-maps.cf"
sudo postconf -e "sender_canonical_classes = envelope_sender,header_sender"
Dovecot을 통해 인증하도록 설정 추가 후 테스트 #
sudo postconf -e "smtpd_sasl_type = dovecot"
sudo postconf -e "smtpd_sasl_path = private/auth"
sudo postconf -e "smtpd_sasl_auth_enable = yes"
sudo postconf -e "smtpd_sasl_security_options = noanonymous"
Test
sudo postmap -q domain1.com mysql:/etc/postfix/mysql-virtual-domains.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-users.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-sender-maps.cf
virtual_domains→1virtual_users→1virtual_sender_maps→email
결과값이 1 → 1 → master@domain1.com 순서로 출력되면 성공입니다.
10-auth.conf 수정 #
sudo nano /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = no
auth_mechanisms = plain login
!include auth-sql.conf.ext
auth-client 소켓 설정 #
sudo nano /etc/dovecot/conf.d/10-master.conf
service anvil {
client_limit = 2000
}
service auth {
client_limit = 2000
process_limit = 1
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
# Dovecot Tuning
service imap-login {
process_limit = 500
client_limit = 1000
}
# LMTP Socket Setting
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
user = postfix
group = postfix
}
}
/etc/dovecot/dovecot.conf 설정 #
sudo nano /etc/dovecot/dovecot.conf
mail_max_userip_connections = 50
protocols = imap lmtp
auth_cache_size = 50M
auth_cache_ttl = 2 hours
ssl = required
/etc/postfix/master.cf 설정 #
sudo nano /etc/postfix/master.cf
smtp inet n - n - - smtpd
submission inet n - n - - smtpd
submissions inet n - n - - smtpd
smtp unix - - n - - smtp
proxymap unix - - n - - proxymap
cleanup unix n - n - 0 cleanup
rewrite unix - - n - - trivial-rewrite
local unix - n n - - local
lmtp unix - - n - - lmtp
/etc/dovecot/conf.d/10-mail.conf 설정 #
sudo nano /etc/dovecot/conf.d/10-mail.conf
# mail_location = maildir:/var/mail/vhosts/%d/%n
# INBOX NAMESPACE
namespace inbox {
type = private
separator = /
prefix =
location = maildir:/var/mail/vhosts/%d/%n
inbox = yes
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Junk {
special_use = \Junk
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
}
maildir_stat_dirs = yes
maildir_copy_with_hardlinks = yes
mail_privileged_group = mail
mail_access_groups = mail
lock_method = fcntl
/etc/dovecot/conf.d/20-lmtp.conf 설정 #
sudo nano /etc/dovecot/conf.d/20-lmtp.conf
protocol lmtp {
postmaster_address = master@domain1.com
mail_plugins = $mail_plugins
}
/etc/mysql/mariadb.conf.d/50-server.cnf 설정 #
MariaDB 성능 튜닝
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
bind-address = 127.0.0.1
# innodb_buffer_pool_size는 서버 RAM 8GB 기준 2G로 설정 (RAM 용량이 다르면 그에 맞게 조정)
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
# MariaDB connection
max_connections = 500
thread_cache_size = 100
table_open_cache = 2000
# slow query (monitoring)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 2
/etc/postfix/main.cf 설정 #
Postfix 동시 처리량
sudo nano /etc/postfix/main.cf
default_process_limit = 200
smtp_destination_concurrency_limit = 20
smtp_destination_rate_delay = 1s
minimal_backoff_time = 300s
maximal_backoff_time = 4000s
queue_run_delay = 300s
smtpd_client_connection_count_limit = 20
smtpd_client_connection_rate_limit = 30
virtual_transport = lmtp:unix:private/dovecot-lmtp
마지막 점검 #
모든 디렉토리를 700으로 (vmail 유저만 접근 가능)
sudo chown -R vmail:vmail /var/mail/vhosts
sudo find /var/mail/vhosts -type d -exec chmod 700 {} +
모든 파일을 600으로 (불필요한 실행 권한 제거)
sudo find /var/mail/vhosts -type f -exec chmod 600 {} +
소켓 파일이 존재하는지 확인 (srw------- 타입)
sudo ls -l /var/spool/postfix/private/dovecot-lmtp
Dovecot이 관리하는 프로토콜 목록 확인
sudo doveconf protocols
결과에 imap lmtp가 포함되어야 함
서비스 재시작 및 점검 #
sudo postfix check
sudo systemctl restart mariadb
sudo systemctl restart dovecot
sudo systemctl restart postfix
DB에러시 확인 #
sudo journalctl -xeu mariadb.service | tail -n 20
# MariaDB 설정 파일 문법 검증 (버전에 따라 이 옵션이 없을 수 있으니,
# 실패하면 아래처럼 dry-run으로 기동 여부만 확인하는 방법도 있습니다)
sudo mariadbd --validate-config
# 대안: sudo mariadbd --help --verbose 로 지원 옵션 목록 확인
sudo doveadm auth test user1@domain1.com
ls -l /var/run/dovecot/auth-userdb
sudo ls -l /var/spool/postfix/private/auth
sudo ls -l /var/spool/postfix/private/dovecot-lmtp
sudo ls -l /var/run/dovecot/auth-client
파일시스템 점검 #
sudo nano /etc/fstab
/dev/disk/by-id/dm-uuid-*** / ext4 defaults,noatime,nodiratime 0 1
/dev/disk/by-uuid/*** /boot ext4 defaults,noatime 0 2
/swap.img none swap sw 0 0
절대 바로 재부팅하지 마세요
sudo mount -a
아무런 메시지도 나오지 않아야 정상입니다.
만약 에러가 난다면 즉시 /etc/fstab을 다시 열어 오타를 수정하세요.
mount | grep -E '/ |/boot'
/dev/mapper/ubuntu--vg-ubuntu--lv on / type ext4 (rw,noatime,nodiratime)
/dev/sda2 on /boot type ext4 (rw,noatime)
적용이 안 됐다면
sudo mount -o remount,noatime /boot
마지막 확인 #
설정 파일 내에서 ssl_cert와 ssl_key 항목 확인
grep -E "ssl_cert|ssl_key" /etc/dovecot/conf.d/10-ssl.conf
Postfix hostname (중요) — 이거 안 맞으면 메일 헤더, TLS, spam score 전부 꼬일 수 있음
sudo nano /etc/hostname
mail.domain1.com
sudo nano /etc/hosts
127.0.0.1 localhost
203.0.113.10 mail.domain1.com mail
sudo nano /etc/postfix/main.cf
myhostname = mail.domain1.com
mydomain = domain1.com
myorigin = $mydomain
결과
hostname
# mail.domain1.com
hostname -f
# mail.domain1.com
postconf myhostname
# mail.domain1.com
인증서 외 #
sudo nano /etc/postfix/main.cf
smtpd_tls_security_level = may
smtp_tls_security_level = may
mydestination = localhost
# Postfix 기본값이지만 명시하는 게 안전 — 없으면 queue 권한 문제가 가끔 발생함
virtual_transport = lmtp:unix:private/dovecot-lmtp
mail_owner = postfix
# 메일 호스팅에서는 기본값(약 50MB)이 문제됨 — 0은 무제한
mailbox_size_limit = 0
# 대용량 첨부 대비 (50MB)
message_size_limit = 52428800
Dovecot auth socket 권한 확인 필수
sudo ls -l /var/spool/postfix/private/auth
정상 예시: srw-rw---- postfix postfix
틀리면 SASL authentication failure 발생함